﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Reflection;
using Us.Mil.Osd.Acq.Damir.Test.Data;
using System.Text.RegularExpressions;

namespace Us.Mil.Osd.Acq.Damir.Test
{
	public class QueryRewriter : ExpressionVisitor
	{
		//QueryMapping mapping;
		/// <summary>
		/// intended to be used to pass table aliases when the visitor finally reaches the base case of ConstantExpression
		/// </summary>
		Dictionary<Expression,ParameterExpression> source_alias_map;		
		Expression root;
		/// <summary>
		/// TODO: this needs to go into the T-SQL translator class
		/// </summary>
		internal static Regex ObjectTypePattern = new Regex(@"Us.Mil.Osd.Acq.Damir\.([a-z|0-9]+)\.([a-z|0-9]+)", RegexOptions.IgnoreCase);
		
		//Stack<string> aliasStack;

		private QueryRewriter() {
			//this.aliasStack = new Stack<string>();
			this.source_alias_map = new Dictionary<Expression, ParameterExpression>();
		}

		public static Expression Rewrite(Expression expression) {			
			QueryRewriter rewriter = new QueryRewriter();
			expression = PartialEvaluator.Eval(expression, canEvaluateLocally);			
			expression = rewriter.Visit(expression);			
			return expression;
		}
		static bool canEvaluateLocally(Expression ex) {
			if (ex.NodeType == ExpressionType.Call) {
				MethodCallExpression m = ((MethodCallExpression)ex);
				for (int i = 0; i < m.Arguments.Count; i++) {
					if ((typeof(IQueryable)).IsAssignableFrom(m.Arguments[i].Type)) {
						return false;
					}
				}
				bool isqueryable = !(typeof(IQueryable)).IsAssignableFrom(ex.Type);
			}
			return ex.NodeType != ExpressionType.Parameter;
		}

		protected override Expression VisitMethodCall(MethodCallExpression m) {
			if (m.Method.DeclaringType == typeof(Queryable) || m.Method.DeclaringType == typeof(Enumerable)) {
				switch (m.Method.Name) {
					case "Where":
						return this.BindWhere(m.Arguments[0], this.GetLambda(m.Arguments[1]));
					case "Select":
						return this.BindSelect(m.Arguments[0], this.GetLambda(m.Arguments[1]));
					case "Join":
						return this.BindJoin(m.Arguments[0], m.Arguments[1], this.GetLambda(m.Arguments[2]), this.GetLambda(m.Arguments[3]), this.GetLambda(m.Arguments[4]));
				}
			}
			return m;
		}

		private SelectExpression BindWhere(Expression source, LambdaExpression predicate) {
			ParameterExpression aliased = predicate.Parameters[0];
			this.source_alias_map[source] = aliased;
			AliasedExpression aliased_source = (AliasedExpression)this.Visit(source);
			Expression where = predicate.Body;
			return new SelectExpression(alias: "T", 
				objectType : aliased_source.Type, 
				from: aliased_source, 
				where: where,
				columns: ProjectColumns(aliased_source.Type, aliased.Name));
		}
		private SelectExpression BindSelect(Expression source, LambdaExpression selector) {
			AliasedExpression aliased_source = (AliasedExpression)this.Visit(source);
			ParameterExpression alias = selector.Parameters[0];
			this.source_alias_map[aliased_source] = alias;
			Expression select = selector.Body;
			return new SelectExpression(alias: "T", //the select Expression itself is not supposed to take on the name of the this alias - the TableExpression is.
				objectType: aliased_source.Type, 
				columns: ProjectColumns(aliased_source.Type, alias.Name),//but project columns off of the alias 
				from: aliased_source, 
				where: null);
			
		}


		/// <summary>
		/// base case where finally the source collection is reached, or some other constant value.
		/// </summary>
		/// <param name="c"></param>
		/// <returns></returns>
		protected override Expression VisitConstant(ConstantExpression c) {			
			if (this.IsQuery(c)) {								
				IQueryable q = (IQueryable)c.Value;				
				Type objectType = TypeHelper.GetElementType(c.Type);				
				return new TableExpression( alias: source_alias_map[c].Name, name: GetTableName(objectType),objectType: objectType, expression: c);				
			}
			return c;
		}

		private Expression BindJoin(Expression first, Expression second, 
			LambdaExpression firstLambda, LambdaExpression secondLambda,  LambdaExpression resultSelector) {

			AliasedExpression outerProjection = (AliasedExpression)this.Visit(first);
			AliasedExpression innerProjection = (AliasedExpression)this.Visit(second);

			this.source_alias_map[first] = firstLambda.Parameters[0];
			this.source_alias_map[second] = secondLambda.Parameters[0];
			Expression firstKey = this.Visit(firstLambda.Body);			
			Expression secondKey = this.Visit(secondLambda.Body);			
			Expression resultExpr = this.Visit(resultSelector.Body);
			JoinExpression join = new JoinExpression(JoinType.InnerJoin, first: first, second: second, condition: Expression.Equal(firstKey, secondKey));			
			return new SelectExpression()
		}

		//TODO: handle "SV" prefix where necessary
		private string GetTableName(Type objectType) {
			Match match = ObjectTypePattern.Match(objectType.FullName);
			return match.Groups[1].Value + "." + match.Groups[2].Value;
		}

		/// <summary>
		/// In most cases just returns * for simplicity. 
		/// Can also select specific columns as specified by the optional LambdaExpression argument.
		/// </summary>
		/// <param name="elementType"></param>
		/// <param name="selector"></param>
		/// <returns></returns>
		private IEnumerable<string> ProjectColumns(Type elementType, string aliasedPrefix, LambdaExpression selector = null) {
			IEnumerable<string> columns = new string[] { "*" };
			if (selector != null) {
				columns = elementType.GetProperties()
				.Where(p => p.Name == selector.Parameters[0].Name)
				.Select(p => p.Name);			
			}			
			return columns.Select(c => aliasedPrefix + "." + c);			
		}
		
		
	}
}
